﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EnterpriseLibraryExtensions.Events.Properties;

namespace EnterpriseLibraryExtensions.Events.Aggregation
{
    /// <summary>
    /// The EventAggregationScope class is used like the TransactionScope class. For best use, always encapsulate any instance
    /// in a using. Events raised within a scope are not send to their respective event handlers until the EventAggregationScope is
    /// disposed and the Complete method has been called.
    /// EventAggregationScopes can be nested and using constructor overloads, nested scopes can be excluded from parents scopes or disabled.
    /// </summary>
    public sealed class EventAggregationScope : IDisposable
    {
        /// <summary>
        /// This static variable is an implementation of the null-object pattern. When no options are desired for an EventAggregationScope, 
        /// assign the options this value.
        /// </summary>
        public static readonly NoFilteringOptions NoFiltering = new NoFilteringOptions();

        /// <summary>
        /// The list of event handlers to raise on a completed dispose.
        /// </summary>
        private readonly List<IEnlistedEventHandler> _enlistedEventHandlers;

        /// <summary>
        /// This static field contains all EventAggregationScopes within a single thread.
        /// </summary>
        [ThreadStatic]
        private static Stack<EventAggregationScope> _enlistedScopes = new Stack<EventAggregationScope>();

        /// <summary>
        /// True if Complete method has been called.
        /// </summary>
        private bool _scopeCompleted;

        /// <summary>
        /// True if a nested scope has been disposed without completing.
        /// </summary>
        private bool _invalidated;

        /// <summary>
        /// Backing field for the Options property.
        /// </summary>
        private IScopeOptions _options;

        /// <summary>
        /// Initializes a new instance of the <see cref="EventAggregationScope"/> class which automatically enlists in the ambient scope
        /// or creates a new scope if none is present.
        /// </summary>
        public EventAggregationScope()
            : this(EventAggregationMode.Required)
        {
            EventAggregationTracer.TraceInformation("EventAggregationScope created");
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EventAggregationScope"/> class.
        /// </summary>
        /// <param name="eventAggregationMode">The event aggregation mode. Using this parameter, it is possible to require a new sub scope or exclude a block of code from the ambient scope.</param>
        public EventAggregationScope(EventAggregationMode eventAggregationMode)
        {
            Mode = eventAggregationMode;
            Options = NoFiltering;

            _invalidated = false;
            _scopeCompleted = false;

            _enlistedEventHandlers = new List<IEnlistedEventHandler>();

            if (_enlistedScopes == null)
            {
                _enlistedScopes = new Stack<EventAggregationScope>();
            }

            _enlistedScopes.Push(this);

            EventAggregationTracer.TraceInformation("EventAggregationScope created. Mode = {0}, Options = {1}", Mode, Options);
        }

        /// <summary>
        /// Gets the current (ambient) EventAggregationScope or null if none is present.
        /// </summary>
        /// <value>The current EventAggregationScope.</value>
        public static EventAggregationScope Current
        {
            get
            {
                return _enlistedScopes != null && _enlistedScopes.Count > 0 ? _enlistedScopes.Peek() : null;
            }
        }

        /// <summary>
        /// Gets the event scope mode. The mode can only be set using the constructor.
        /// </summary>
        /// <value>The mode the scope is to operate under.</value>
        public EventAggregationMode Mode { get; private set; }

        /// <summary>
        /// Gets or sets the options for the EventAggregationScope. 
        /// To specify no options, assign Options the value NoFiltering.
        /// </summary>
        /// <value>The options for the scope. The value cannot be null.</value>
        /// <exception cref="System.InvalidOperationException">Thrown if value is null.</exception>
        public IScopeOptions Options
        {
            get
            {
                return _options;
            }
            set
            {
                if (value == null)
                {
                    throw new InvalidOperationException(Resources.EventAggregationScopeOptionsCannotBeNull);
                }

                _options = value;

                EventAggregationTracer.TraceInformation("Options property set. New value: {0}", _options);
            }
        }

        /// <summary>
        /// Completes this instance and signals the scope that it must raise events on dispose. Failing to call
        /// complete before disposing of the scope will prevent events from being raised - unless the scope's mode is disabled.
        /// </summary>
        public void Complete()
        {
            _scopeCompleted = true;

            EventAggregationTracer.TraceInformation("Scope has been marked completed");
        }

        /// <summary>
        /// When the scope is disposed and Complete has been called, all events aggregated in the scope will be raised.
        /// If complete has not been called and mode is disabled, events will be raised anyway.
        /// </summary>
        public void Dispose()
        {
            EventAggregationTracer.TraceInformation("Scope dispose beginning");

            GC.SuppressFinalize(this);

            var nextInStack = PopAndPeekNextInStack();

            ///////////////////////////////////////////////////////////////////
            // Handle scope not completed.
            ///////////////////////////////////////////////////////////////////

            if (Mode != EventAggregationMode.Disable && (!_scopeCompleted || _invalidated))
            {
                if (Mode == EventAggregationMode.Required && nextInStack != null)
                {
                    InvalidateOuterScope(nextInStack);
                }

                EventAggregationTracer.TraceInformation("Scope was not completed or was invalidated. No further actions.");

                return;
            }

            ///////////////////////////////////////////////////////////////////
            // Handle scope completed.
            ///////////////////////////////////////////////////////////////////

            bool raiseEvents = nextInStack == null;

            if (Mode == EventAggregationMode.Disable ||
                Mode == EventAggregationMode.RequireNew)
            {
                raiseEvents = true;
            }

            if (nextInStack != null && !raiseEvents)
            {
                nextInStack._enlistedEventHandlers.AddRange(this._enlistedEventHandlers);
            }

            if (raiseEvents)
            {
                EventAggregationTracer.TraceInformation("Scope will raise events.");

                var filteredEventHandlers = Options.RemoveDuplicates(_enlistedEventHandlers);
                _enlistedEventHandlers.Clear();
                _enlistedEventHandlers.AddRange(filteredEventHandlers);

                foreach (var enlistedEventHandler in _enlistedEventHandlers)
                {
                    EventAggregationTracer.TraceInformation("Raising event handler " + enlistedEventHandler.EventHandler);
                    
                    enlistedEventHandler.EventHandler.OnEvent(enlistedEventHandler.Sender, enlistedEventHandler.Args);
                }
            }
        }

        /// <summary>
        /// Method enlists the provided event handlers to the current ambient scope. If scope is in disable mode,
        /// the events are raised at once. If not, events are enlisted for later use.
        /// </summary>
        /// <param name="sender">The instance that raised the event.</param>
        /// <param name="args">The event arguments.</param>
        /// <param name="eventHandlers">The eventHandlers to handle the event.</param>
        internal static void Enlist(object sender, object args, IEnumerable<IHandlesEvent> eventHandlers)
        {
            EventAggregationTracer.TraceInformation("Enlisting event handlers");

            if (Current != null && Current.Mode == EventAggregationMode.Disable)
            {
                EventAggregationTracer.TraceInformation("Scope is disabled. Raising events at once.");

                Raise(sender, args, eventHandlers);
                return;
            }

            if (Current != null)
            {
                foreach (var eventHandler in eventHandlers)
                {
                    Current._enlistedEventHandlers.Add(new EnlistedEventHandler(sender, args, eventHandler));
                }

                EventAggregationTracer.TraceInformation("Event handlers enlisted.");

                return;
            }

            Raise(sender, args, eventHandlers);
        }

        /// <summary>
        /// Invokes all passed event handlers.
        /// </summary>
        /// <param name="sender">The instance that originally raised the event.</param>
        /// <param name="args">The arguments that will be passed to all event handlers.</param>
        /// <param name="eventHandlers">The event handlers to be invoked.</param>
        internal static void Raise(object sender, object args, IEnumerable<IHandlesEvent> eventHandlers)
        {
            EventAggregationTracer.TraceInformation("Raising async event handlers.");

            Parallel.ForEach(eventHandlers.Where(eh => eh.ExecuteAsync), handlesEvent => handlesEvent.OnEvent(sender, args));

            EventAggregationTracer.TraceInformation("Raising sync event handlers.");

            foreach (var eventHandler in eventHandlers.Where(eh => !eh.ExecuteAsync))
            {
                eventHandler.OnEvent(sender, args);
            }
        }

        /// <summary>
        /// Invalidates the scope passed to the method. Invalidating the scope means that it
        /// will not invoke its event handlers.
        /// </summary>
        /// <param name="nextInStack">The scope to mark invalidated.</param>
        private static void InvalidateOuterScope(EventAggregationScope nextInStack)
        {
            nextInStack._invalidated = true;

            EventAggregationTracer.TraceInformation("Outer scope invalidated");
        }

        /// <summary>
        /// Pops the and peek next event aggregation scope in stack the stack.
        /// </summary>
        /// <returns>The next scope in the stack or null if the current scope is the outer most.</returns>
        private EventAggregationScope PopAndPeekNextInStack()
        {
            var popped = _enlistedScopes.Pop();

            if (popped != this)
            {
                throw new InvalidOperationException(Resources.EventAggregationScopeInconsistentDisposeOrder);
            }

            EventAggregationTracer.TraceInformation("Popped and peeked next scope in stack");

            return _enlistedScopes.Count == 0 ? null : _enlistedScopes.Peek();
        }
    }
}
